package net.minecraft.client.font;

import com.ibm.icu.text.ArabicShaping;
import com.ibm.icu.text.ArabicShapingException;
import com.ibm.icu.text.Bidi;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.ScreenRect;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.text.CharacterVisitor;
import net.minecraft.text.OrderedText;
import net.minecraft.text.StringVisitable;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.text.TextColor;
import net.minecraft.text.TextVisitFactory;
import net.minecraft.util.Identifier;
import net.minecraft.util.Language;
import net.minecraft.util.math.ColorHelper;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.random.Random;
import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;

/**
 * Manages the rendering of text.
 * 
 * <p>The current instance used by the client can be obtained by
 * {@code MinecraftClient.getInstance().textRenderer}.
 * 
 * @see net.minecraft.client.MinecraftClient#textRenderer
 */
@Environment(EnvType.CLIENT)
public class TextRenderer {
	private static final float Z_INDEX = 0.01F;
	private static final float field_60693 = 0.01F;
	private static final float field_60694 = -0.01F;
	public static final float FORWARD_SHIFT = 0.03F;
	public static final int field_55090 = 0;
	/**
	 * The font height of the text that is rendered by the text renderer.
	 */
	public final int fontHeight = 9;
	public final Random random = Random.create();
	private final Function<Identifier, FontStorage> fontStorageAccessor;
	final boolean validateAdvance;
	private final TextHandler handler;

	public TextRenderer(Function<Identifier, FontStorage> fontStorageAccessor, boolean validateAdvance) {
		this.fontStorageAccessor = fontStorageAccessor;
		this.validateAdvance = validateAdvance;
		this.handler = new TextHandler(
			(codePoint, style) -> this.getFontStorage(style.getFont()).getGlyph(codePoint, this.validateAdvance).getAdvance(style.isBold())
		);
	}

	FontStorage getFontStorage(Identifier id) {
		return (FontStorage)this.fontStorageAccessor.apply(id);
	}

	public String mirror(String text) {
		try {
			Bidi bidi = new Bidi(new ArabicShaping(8).shape(text), 127);
			bidi.setReorderingMode(0);
			return bidi.writeReordered(2);
		} catch (ArabicShapingException var3) {
			return text;
		}
	}

	/**
	 * @param color the text color in the 0xAARRGGBB format
	 */
	public void draw(
		String string,
		float x,
		float y,
		int color,
		boolean shadow,
		Matrix4f matrix,
		VertexConsumerProvider vertexConsumers,
		TextRenderer.TextLayerType layerType,
		int backgroundColor,
		int light
	) {
		TextRenderer.GlyphDrawable glyphDrawable = this.prepare(string, x, y, color, shadow, backgroundColor);
		glyphDrawable.draw(TextRenderer.GlyphDrawer.drawing(vertexConsumers, matrix, layerType, light));
	}

	/**
	 * @param color the text color in the 0xAARRGGBB format
	 */
	public void draw(
		Text text,
		float x,
		float y,
		int color,
		boolean shadow,
		Matrix4f matrix,
		VertexConsumerProvider vertexConsumers,
		TextRenderer.TextLayerType layerType,
		int backgroundColor,
		int light
	) {
		TextRenderer.GlyphDrawable glyphDrawable = this.prepare(text.asOrderedText(), x, y, color, shadow, backgroundColor);
		glyphDrawable.draw(TextRenderer.GlyphDrawer.drawing(vertexConsumers, matrix, layerType, light));
	}

	/**
	 * @param color the text color in the 0xAARRGGBB format
	 */
	public void draw(
		OrderedText text,
		float x,
		float y,
		int color,
		boolean shadow,
		Matrix4f matrix,
		VertexConsumerProvider vertexConsumers,
		TextRenderer.TextLayerType layerType,
		int backgroundColor,
		int light
	) {
		TextRenderer.GlyphDrawable glyphDrawable = this.prepare(text, x, y, color, shadow, backgroundColor);
		glyphDrawable.draw(TextRenderer.GlyphDrawer.drawing(vertexConsumers, matrix, layerType, light));
	}

	/**
	 * @param outlineColor the outline color in 0xAARRGGBB
	 * @param color the text color in 0xAARRGGBB
	 */
	public void drawWithOutline(
		OrderedText text, float x, float y, int color, int outlineColor, Matrix4f matrix, VertexConsumerProvider vertexConsumers, int light
	) {
		TextRenderer.Drawer drawer = new TextRenderer.Drawer(0.0F, 0.0F, outlineColor, false);

		for (int i = -1; i <= 1; i++) {
			for (int j = -1; j <= 1; j++) {
				if (i != 0 || j != 0) {
					float[] fs = new float[]{x};
					int k = i;
					int l = j;
					text.accept((index, style, codePoint) -> {
						boolean bl = style.isBold();
						FontStorage fontStorage = this.getFontStorage(style.getFont());
						Glyph glyph = fontStorage.getGlyph(codePoint, this.validateAdvance);
						drawer.x = fs[0] + k * glyph.getShadowOffset();
						drawer.y = y + l * glyph.getShadowOffset();
						fs[0] += glyph.getAdvance(bl);
						return drawer.accept(index, style.withColor(outlineColor), codePoint);
					});
				}
			}
		}

		TextRenderer.GlyphDrawer glyphDrawer = TextRenderer.GlyphDrawer.drawing(vertexConsumers, matrix, TextRenderer.TextLayerType.NORMAL, light);

		for (BakedGlyph.DrawnGlyph drawnGlyph : drawer.drawnGlyphs) {
			glyphDrawer.drawGlyph(drawnGlyph);
		}

		TextRenderer.Drawer drawer2 = new TextRenderer.Drawer(x, y, color, false);
		text.accept(drawer2);
		drawer2.draw(TextRenderer.GlyphDrawer.drawing(vertexConsumers, matrix, TextRenderer.TextLayerType.POLYGON_OFFSET, light));
	}

	public TextRenderer.GlyphDrawable prepare(String string, float x, float y, int color, boolean shadow, int backgroundColor) {
		if (this.isRightToLeft()) {
			string = this.mirror(string);
		}

		TextRenderer.Drawer drawer = new TextRenderer.Drawer(x, y, color, backgroundColor, shadow);
		TextVisitFactory.visitFormatted(string, Style.EMPTY, drawer);
		return drawer;
	}

	public TextRenderer.GlyphDrawable prepare(OrderedText text, float x, float y, int color, boolean shadow, int backgroundColor) {
		TextRenderer.Drawer drawer = new TextRenderer.Drawer(x, y, color, backgroundColor, shadow);
		text.accept(drawer);
		return drawer;
	}

	/**
	 * Gets the width of some text when rendered.
	 * 
	 * @param text the text
	 */
	public int getWidth(String text) {
		return MathHelper.ceil(this.handler.getWidth(text));
	}

	/**
	 * Gets the width of some text when rendered.
	 * 
	 * @param text the text
	 */
	public int getWidth(StringVisitable text) {
		return MathHelper.ceil(this.handler.getWidth(text));
	}

	/**
	 * Gets the width of some text when rendered.
	 */
	public int getWidth(OrderedText text) {
		return MathHelper.ceil(this.handler.getWidth(text));
	}

	/**
	 * Trims a string to be at most {@code maxWidth} wide.
	 * 
	 * @return the trimmed string
	 */
	public String trimToWidth(String text, int maxWidth, boolean backwards) {
		return backwards ? this.handler.trimToWidthBackwards(text, maxWidth, Style.EMPTY) : this.handler.trimToWidth(text, maxWidth, Style.EMPTY);
	}

	/**
	 * Trims a string to be at most {@code maxWidth} wide.
	 * 
	 * @return the trimmed string
	 * @see TextHandler#trimToWidth(String, int, Style)
	 */
	public String trimToWidth(String text, int maxWidth) {
		return this.handler.trimToWidth(text, maxWidth, Style.EMPTY);
	}

	/**
	 * Trims a string to be at most {@code maxWidth} wide.
	 * 
	 * @return the text
	 * @see TextHandler#trimToWidth(StringVisitable, int, Style)
	 */
	public StringVisitable trimToWidth(StringVisitable text, int width) {
		return this.handler.trimToWidth(text, width, Style.EMPTY);
	}

	/**
	 * Gets the height of the text when it has been wrapped.
	 * 
	 * @return the height of the wrapped text
	 * @see TextRenderer#wrapLines(StringVisitable, int)
	 * @see #getWrappedLinesHeight(StringVisitable, int)
	 */
	public int getWrappedLinesHeight(String text, int maxWidth) {
		return 9 * this.handler.wrapLines(text, maxWidth, Style.EMPTY).size();
	}

	/**
	 * {@return the height of the text, after it has been wrapped, in pixels}
	 * @see TextRenderer#wrapLines(StringVisitable, int)
	 * @see #getWrappedLinesHeight(String, int)
	 */
	public int getWrappedLinesHeight(StringVisitable text, int maxWidth) {
		return 9 * this.handler.wrapLines(text, maxWidth, Style.EMPTY).size();
	}

	/**
	 * Wraps text when the rendered width of text exceeds the {@code width}.
	 * 
	 * @return a list of ordered text which has been wrapped
	 */
	public List<OrderedText> wrapLines(StringVisitable text, int width) {
		return Language.getInstance().reorder(this.handler.wrapLines(text, width, Style.EMPTY));
	}

	public List<StringVisitable> wrapLinesWithoutLanguage(StringVisitable text, int width) {
		return this.handler.wrapLines(text, width, Style.EMPTY);
	}

	/**
	 * Checks if the currently set language uses right to left writing.
	 */
	public boolean isRightToLeft() {
		return Language.getInstance().isRightToLeft();
	}

	public TextHandler getTextHandler() {
		return this.handler;
	}

	@Environment(EnvType.CLIENT)
	class Drawer implements CharacterVisitor, TextRenderer.GlyphDrawable {
		private final boolean shadow;
		private final int color;
		private final int backgroundColor;
		float x;
		float y;
		private float minX = Float.MAX_VALUE;
		private float minY = Float.MAX_VALUE;
		private float maxX = -Float.MAX_VALUE;
		private float maxY = -Float.MAX_VALUE;
		private float minBackgroundX = Float.MAX_VALUE;
		private float minBackgroundY = Float.MAX_VALUE;
		private float maxBackgroundX = -Float.MAX_VALUE;
		private float maxBackgroundY = -Float.MAX_VALUE;
		final List<BakedGlyph.DrawnGlyph> drawnGlyphs = new ArrayList();
		@Nullable
		private List<BakedGlyph.Rectangle> rectangles;

		public Drawer(final float x, final float y, final int color, final boolean shadow) {
			this(x, y, color, 0, shadow);
		}

		public Drawer(final float x, final float y, final int color, final int backgroundColor, final boolean shadow) {
			this.x = x;
			this.y = y;
			this.shadow = shadow;
			this.color = color;
			this.backgroundColor = backgroundColor;
			this.updateBackgroundBounds(x, y, 0.0F);
		}

		private void updateTextBounds(float minX, float minY, float maxX, float maxY) {
			this.minX = Math.min(this.minX, minX);
			this.minY = Math.min(this.minY, minY);
			this.maxX = Math.max(this.maxX, maxX);
			this.maxY = Math.max(this.maxY, maxY);
		}

		private void updateBackgroundBounds(float x, float y, float width) {
			if (ColorHelper.getAlpha(this.backgroundColor) != 0) {
				this.minBackgroundX = Math.min(this.minBackgroundX, x - 1.0F);
				this.minBackgroundY = Math.min(this.minBackgroundY, y - 1.0F);
				this.maxBackgroundX = Math.max(this.maxBackgroundX, x + width);
				this.maxBackgroundY = Math.max(this.maxBackgroundY, y + 9.0F);
				this.updateTextBounds(this.minBackgroundX, this.minBackgroundY, this.maxBackgroundX, this.maxBackgroundY);
			}
		}

		private void addGlyph(BakedGlyph.DrawnGlyph glyph) {
			this.drawnGlyphs.add(glyph);
			this.updateTextBounds(glyph.getEffectiveMinX(), glyph.getEffectiveMinY(), glyph.getEffectiveMaxX(), glyph.getEffectiveMaxY());
		}

		private void addRectangle(BakedGlyph.Rectangle rectangle) {
			if (this.rectangles == null) {
				this.rectangles = new ArrayList();
			}

			this.rectangles.add(rectangle);
			this.updateTextBounds(rectangle.getEffectiveMinX(), rectangle.getEffectiveMinY(), rectangle.getEffectiveMaxX(), rectangle.getEffectiveMaxY());
		}

		@Override
		public boolean accept(int i, Style style, int j) {
			FontStorage fontStorage = TextRenderer.this.getFontStorage(style.getFont());
			Glyph glyph = fontStorage.getGlyph(j, TextRenderer.this.validateAdvance);
			BakedGlyph bakedGlyph = style.isObfuscated() && j != 32 ? fontStorage.getObfuscatedBakedGlyph(glyph) : fontStorage.getBaked(j);
			boolean bl = style.isBold();
			TextColor textColor = style.getColor();
			int k = this.getRenderColor(textColor);
			int l = this.getShadowColor(style, k);
			float f = glyph.getAdvance(bl);
			float g = i == 0 ? this.x - 1.0F : this.x;
			float h = glyph.getShadowOffset();
			if (!(bakedGlyph instanceof EmptyBakedGlyph)) {
				float m = bl ? glyph.getBoldOffset() : 0.0F;
				this.addGlyph(new BakedGlyph.DrawnGlyph(this.x, this.y, k, l, bakedGlyph, style, m, h));
			}

			this.updateBackgroundBounds(this.x, this.y, f);
			if (style.isStrikethrough()) {
				this.addRectangle(new BakedGlyph.Rectangle(g, this.y + 4.5F - 1.0F, this.x + f, this.y + 4.5F, 0.01F, k, l, h));
			}

			if (style.isUnderlined()) {
				this.addRectangle(new BakedGlyph.Rectangle(g, this.y + 9.0F - 1.0F, this.x + f, this.y + 9.0F, 0.01F, k, l, h));
			}

			this.x += f;
			return true;
		}

		@Override
		public void draw(TextRenderer.GlyphDrawer glyphDrawer) {
			BakedGlyph bakedGlyph = null;
			if (ColorHelper.getAlpha(this.backgroundColor) != 0) {
				BakedGlyph.Rectangle rectangle = new BakedGlyph.Rectangle(
					this.minBackgroundX, this.minBackgroundY, this.maxBackgroundX, this.maxBackgroundY, -0.01F, this.backgroundColor
				);
				bakedGlyph = TextRenderer.this.getFontStorage(Style.DEFAULT_FONT_ID).getRectangleBakedGlyph();
				glyphDrawer.drawRectangle(bakedGlyph, rectangle);
			}

			for (BakedGlyph.DrawnGlyph drawnGlyph : this.drawnGlyphs) {
				glyphDrawer.drawGlyph(drawnGlyph);
			}

			if (this.rectangles != null) {
				if (bakedGlyph == null) {
					bakedGlyph = TextRenderer.this.getFontStorage(Style.DEFAULT_FONT_ID).getRectangleBakedGlyph();
				}

				for (BakedGlyph.Rectangle rectangle2 : this.rectangles) {
					glyphDrawer.drawRectangle(bakedGlyph, rectangle2);
				}
			}
		}

		private int getRenderColor(@Nullable TextColor override) {
			if (override != null) {
				int i = ColorHelper.getAlpha(this.color);
				int j = override.getRgb();
				return ColorHelper.withAlpha(i, j);
			} else {
				return this.color;
			}
		}

		private int getShadowColor(Style style, int textColor) {
			Integer integer = style.getShadowColor();
			if (integer != null) {
				float f = ColorHelper.getAlphaFloat(textColor);
				float g = ColorHelper.getAlphaFloat(integer);
				return f != 1.0F ? ColorHelper.withAlpha(ColorHelper.channelFromFloat(f * g), integer) : integer;
			} else {
				return this.shadow ? ColorHelper.scaleRgb(textColor, 0.25F) : 0;
			}
		}

		@Nullable
		@Override
		public ScreenRect getScreenRect() {
			if (!(this.minX >= this.maxX) && !(this.minY >= this.maxY)) {
				int i = MathHelper.floor(this.minX);
				int j = MathHelper.floor(this.minY);
				int k = MathHelper.ceil(this.maxX);
				int l = MathHelper.ceil(this.maxY);
				return new ScreenRect(i, j, k - i, l - j);
			} else {
				return null;
			}
		}
	}

	@Environment(EnvType.CLIENT)
	public interface GlyphDrawable {
		void draw(TextRenderer.GlyphDrawer glyphDrawer);

		@Nullable
		ScreenRect getScreenRect();
	}

	@Environment(EnvType.CLIENT)
	public interface GlyphDrawer {
		static TextRenderer.GlyphDrawer drawing(VertexConsumerProvider vertexConsumers, Matrix4f matrix, TextRenderer.TextLayerType layerType, int light) {
			return new TextRenderer.GlyphDrawer() {
				@Override
				public void drawGlyph(BakedGlyph.DrawnGlyph glyph) {
					BakedGlyph bakedGlyph = glyph.glyph();
					VertexConsumer vertexConsumer = vertexConsumers.getBuffer(bakedGlyph.getLayer(layerType));
					bakedGlyph.draw(glyph, matrix, vertexConsumer, light, false);
				}

				@Override
				public void drawRectangle(BakedGlyph bakedGlyph, BakedGlyph.Rectangle rect) {
					VertexConsumer vertexConsumer = vertexConsumers.getBuffer(bakedGlyph.getLayer(layerType));
					bakedGlyph.drawRectangle(rect, matrix, vertexConsumer, light, false);
				}
			};
		}

		void drawGlyph(BakedGlyph.DrawnGlyph glyph);

		void drawRectangle(BakedGlyph bakedGlyph, BakedGlyph.Rectangle rect);
	}

	@Environment(EnvType.CLIENT)
	public static enum TextLayerType {
		NORMAL,
		SEE_THROUGH,
		POLYGON_OFFSET;
	}
}
